import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;

public class Scene {

	private int height=0;
	private int width=0;
	
	private RGB backgroundCol;
	private String backgroundTex;
	private RGB ambientLight;
	private int superSampWidth;
	private boolean useAcceleration;
	
	private Camera camera = null;
	private ArrayList<Light> lights = null;
	private ArrayList<Primitive> primitivs = null;
	
	private OctreeNode root = null;
	
	public Scene(){
		this.backgroundCol = new RGB();
		this.backgroundTex = null;
		this.ambientLight = new RGB();
		this.superSampWidth = 1;
		this.useAcceleration = false;
		
		this.camera = new Camera();
		this.lights = new ArrayList<Light>();
		this.primitivs = new ArrayList<Primitive>();
	}
	
	public void setCanvasSize(int height, int width) {
		this.height=height;
		this.width=width;
	}
	
	public void setBackgroundCol(RGB bgColor){
		this.backgroundCol = bgColor;
	}
	public RGB getBackgroundCol(){
		return this.backgroundCol;
	}
	
	public void setBckgroundTex(String bgText){
		this.backgroundTex = bgText;
	}
	public String getBckgroundTex(){
		return this.backgroundTex;
	}
	
	public RGB getBckgroundAt(){
		//if (this.getBckgroundTex()==null)
			return this.getBackgroundCol();
		
		
	}
	
	public void setAmbientLight(RGB amb){
		this.ambientLight = amb;
	}
	public RGB getAmbientLight(){
		return this.ambientLight;
	}
	
	public void setSuperSampWidth(int width){
		this.superSampWidth = width;
	}
	public int getSuperSampWidth(){
		return this.superSampWidth;
	}
	
	public void setUseAcceleration(boolean use){
		this.useAcceleration = use;
	}
	public boolean getUseAcceleration(){
		return this.useAcceleration;
	}
	
	//Camera:
	public void setCamera(Camera cam){
		this.camera = cam;
	}
	public Camera getCamera(){
		return camera;
	}	

	// Lights:
	public void addLight(Light light) {
		if (light instanceof AreaLight)
			this.addAreaLight(light);
		else 
			this.lights.add(light);
	}
	
	public void addAreaLight(Light light) {
		AreaLight l = (AreaLight)light;
		l.setColor(light.getColor());
		Vector p0 = l.getP0();
		Vector p1 = l.getP1();
		Vector p2 = l.getP2();
		int N = l.getGridWidth();
		Vector delta1 = new Vector();
		Vector delta2 = new Vector();
		if (N>1) {
			delta1 = Vector.sub(p1, p0).mul(1.0/(N-1));
			delta2 = Vector.sub(p2, p0).mul(1.0/(N-1));
		}
		
		for (int i=0; i<N ; i++) {
			
			 for (int j=0; j<N; j++) {
				 PointLight PL = new PointLight();
				 PL.setPos(p0);
				 PL.setColor(l.getColor().mul(1.0));
				 this.lights.add(PL);
				 p0 = Vector.add(p0, delta2);
			 }
			 p0 = Vector.add(p0, delta1);
		}
		this.lights.remove(this.lights.indexOf(light));
	}
	
	
	
	//Primitives:
	public void addPrimitive( Primitive primitive){
		this.primitivs.add(primitive);
	}
	
	public int getCanvasRGBat(int x, int y){		
		
		double realWidth = this.camera.getScreenWidth();
		double realHeight = realWidth * this.height / this.width;
		double realX = (x + 0.5)*realWidth / this.width;
		double realY = (y + 0.5)*realHeight / this.height;
		Vector right = Vector.crossProduct(this.camera.getDirection(), this.camera.getUp());
		Vector topLeft = Vector.add(this.camera.getEye(), 
							Vector.add(this.camera.getDirection().mul(this.camera.getScreenDist()), 
								Vector.add(this.camera.getUp().mul(realHeight/2),
									right.mul(- realWidth/2))));
		Vector down = this.camera.getUp().mul(-1);
		Vector sample = Vector.add(topLeft,
							Vector.add(right.mul(realX),down.mul(realY)));
		return getRGBthrough(sample, realWidth/this.width, realHeight/this.height, right, down);
	}
	
	public int getCanvasRGBat(int x, int y, int canvasHeight, int canvasWidth){
		this.height = canvasHeight;
		this.width = canvasWidth;
		return getCanvasRGBat(x, y);
	}
	
	private int getRGBthrough(Vector middle, double pixelWidth, double pixelHeight, Vector right, Vector down){
		RGB rgb = new RGB();
		Vector temp = null;
		RGB tempRGB = null;
		double tempWeight = 0.0;
		double totalWeight = 0.0;
		double realX;
		double realY;
		double dist;
		
		Vector topLeft = Vector.add(middle, 
				Vector.add(right.mul( - pixelWidth/2), down.mul( - pixelHeight/2)));
		
		for(int i=0; i<this.superSampWidth; i++){
			for (int j=0; j<this.superSampWidth; j++){
				realX = (i + 0.5)*pixelWidth / superSampWidth;
				realY = (j + 0.5)*pixelHeight / superSampWidth;
				temp = Vector.add(topLeft, 
						Vector.add(right.mul( realX), down.mul( realY))); 
				tempRGB = shootSingleRay_through(camera.getEye(), temp, 4);
				dist = Vector.getDistance(temp, middle);
				tempWeight = weightFunction(dist);
				rgb = RGB.add(rgb, tempRGB.mul(tempWeight));
				totalWeight+= tempWeight;
			}
		}
		
		rgb = rgb.mul(1/totalWeight);
		return rgb.getRGB();
	}
	
	
	private RGB shootSingleRay_through(Vector origin, Vector through, int recurtionDepth){		
		Vector direction = Vector.sub_n(through, origin);
		return shootSingleRay_direction(origin, direction, recurtionDepth);
	}
	
	
	private RGB shootSingleRay_direction(Vector origin, Vector direction, int recurtionDepth){
		
		if (recurtionDepth == 0) 
			return this.backgroundCol;
		
		Enumeration<Primitive> e = Collections.enumeration(primitivs);
		Primitive p;
		double shortestDist_Q = Double.MAX_VALUE;
		double dist_Q;
		Primitive closestP = null;
		Vector closestV = null;
		Vector temp = null;
		
		while(e.hasMoreElements()){
		     p = e.nextElement();
		     temp = p.intersectionPoint_direction(origin, direction);
		     if (temp == null || temp.isEqual_n(origin)) 
		    	 continue;
		     dist_Q = Vector.getDistance_Q(temp, origin);
		     if (dist_Q < shortestDist_Q){
		    	 shortestDist_Q = dist_Q;
		    	 closestP = p;
		    	 closestV = temp;
		     }
		}
		
		if (closestV == null) 
			return this.backgroundCol;
		
		return getSceneRGBat(closestP, closestV, direction,recurtionDepth);
	}
	
	private RGB getSceneRGBat(Primitive p, Vector target, Vector viewDirection, int recurtionDepth){
		
		Enumeration<Light> e = Collections.enumeration(lights);
		Light lightSource;
		RGB I, Ie, Ka, Ial, Kd, Il, Ks, Ir;
		Vector V, N, L, R;
		double n, Kt;
		boolean Sl;
		RGB sum = new RGB();
		
		
		Ie = p.getMtlEmission();
		Ka = p.getMtlAmbient();
		Ial = this.ambientLight;
		Kd = p.getMtlDiffuse();
		Ks = p.getMtlSpecular();
		V = viewDirection.mul(-1);
		n = p.getMtlShininess();
		Kt = p.getReflectance();
		N = p.getNormalAt(target, viewDirection);
		
		while(e.hasMoreElements()){
		    lightSource = e.nextElement();		
		    Sl = clearView(target, viewDirection, lightSource);
			if(Sl == true){
				L = lightSource.getDirection(target).mul(-1);
				Il = lightSource.getLightningAt(target);
				R = Vector.getVector_sameAngle_samePlain(N, L);								
				sum = RGB.add(sum,
						RGB.add( RGB.mulRGB(Kd, Il).mul(Vector.cosAngle(N, L)),
							RGB.mulRGB(Ks, Il).mul(Math.pow(Vector.cosAngle(V, R), n))));
							
			}
		}
		Ir = new RGB();
		if (Kt != 0.0){
			Ir = shootSingleRay_direction(target, Vector.getVector_sameAngle_samePlain(N, V), recurtionDepth - 1);
		}
		
		//global ilumination (or not)
		RGB globalLight;
		RGB allButGlobalLight;
		boolean globalIlumination = true;
		if (globalIlumination){
			globalLight = calc_global_illumination_at(target,N,recurtionDepth);
			
			allButGlobalLight = RGB.add(Ie,	
							RGB.add(sum, Ir.mul(Kt)));
			
			I = RGB.add(RGB.mulRGB(globalLight, Kd),allButGlobalLight);
		}else{
			
			I = RGB.add(Ie,
					RGB.add(RGB.mulRGB(Ka, Ial),	
						RGB.add(sum, Ir.mul(Kt))));
		}
		
		
		return I;
		
	}
	
	private boolean clearView(Vector source, Vector viewDirection, Light l){
		Enumeration<Primitive> e = Collections.enumeration(primitivs);
		Primitive p;
		while(e.hasMoreElements()){
		     p = e.nextElement();
		     if (l.isBlockedByPrimitive(source, viewDirection, p)) return false;
		}
		return true;
	}
	
	
	private static double weightFunction( double dist){
		return gaussian(0,10.0,dist);
	}
	
	private static double gaussian( double mean, double sd, double dist){
		double a = 1/(sd* Math.sqrt(2*Math.PI));
		return a * Math.pow(Math.E, -( (dist-mean)*(dist-mean)/ 2*sd*sd )  );
	}
	
	private RGB calc_global_illumination_at(Vector origin, Vector normal, int recurtionDepth){
		double raysToShot = 1;
		Vector randomDirection;
		RGB temp;
		RGB total = new RGB(0.0,0.0,0.0);
		if (recurtionDepth == 3) raysToShot = 1;
		if (recurtionDepth == 2) raysToShot = 16;
		if (recurtionDepth == 1) raysToShot = 8;
		for (int i=0; i<raysToShot; i++){
			randomDirection = Vector.generate_RandomVector_outOfPlain(origin, normal);
			//System.out.println(randomDirection.print());
			temp = shootSingleRay_direction(origin, randomDirection, recurtionDepth - 1);
			total = RGB.add(total, temp.mul(1/raysToShot));
		}
		return total;
	}
	
}